﻿using System;
using System.Collections.Generic;
using System.Linq;
using Simp.CodeModel;
using Simp.CodeModel.CoreFunctions;
using Simp.CodeModel.CoreVisitors;
using Simp.CodeModel.Exceptions;

namespace Simp.CodeModel
{
	/// <summary>Evaluation context accumulating assignations and able to evaluate expressions using those variables.</summary>
	public class EvaluationContext
	{
		private readonly List<IFunction> _functionList = new List<IFunction>();
		private readonly IDictionary<string, IFunction> _functionIndex = new Dictionary<string, IFunction>();

		/// <summary>Private constructor.</summary>
		/// <param name="packageList"/>
		private EvaluationContext(params IFunction[][] packageList)
		{
			//	Loading each package list
			Array.ForEach(packageList, delegate(IFunction[] functionList)
			{
				Array.ForEach(functionList, delegate(IFunction function)
				{
					_functionIndex.Add(function.FunctionName, function);
				});
			});
		}

		/// <summary>Loads an <see cref="EvaluationContext"/> with no known functions.</summary>
		/// <returns></returns>
		public static EvaluationContext WithNoKnownFunctions()
		{
			return WithPackages();
		}

		/// <summary>Loads an <see cref="EvaluationContext"/> with only the core functions.</summary>
		/// <returns></returns>
		public static EvaluationContext WithCorePackage()
		{
			return WithPackages(CorePackage.Package);
		}

		/// <summary>Loads an <see cref="EvaluationContext"/> with given function packages.</summary>
		/// <param name="packageList"></param>
		/// <returns></returns>
		public static EvaluationContext WithPackages(params IFunction[][] packageList)
		{
			if (packageList == null)
			{
				throw new ArgumentNullException("packageList");
			}

			return new EvaluationContext(packageList);
		}

		/// <summary>List of currently parsed functions.</summary>
		public IFunction[] KnownFunctions
		{
			get { return _functionList.ToArray(); }
		}

		/// <summary>Integrate a function in the context.</summary>
		/// <param name="function"></param>
		/// <returns>Returns the simplified function.</returns>
		public IFunction Integrate(IFunction function)
		{
			if (function == null)
			{
				throw new ArgumentNullException("function");
			}
			if (function is CustomFunction)
			{
				if (_functionIndex.ContainsKey(function.FunctionName))
				{
					throw new FunctionAlreadyDefinedException(function.FunctionName);
				}

				IFunction evaluatedFunction = Evaluate(function, false);

				_functionList.Add(evaluatedFunction);
				_functionIndex.Add(evaluatedFunction.FunctionName, evaluatedFunction);

				return evaluatedFunction;
			}
			else
			{
				return Evaluate(function, true);
			}
		}

		/// <summary>Integrate a collection of functions in the context.</summary>
		/// <param name="functionList"></param>
		public IFunction[] Integrate(IEnumerable<IFunction> functionList)
		{
			List<IFunction> evaluatedFunctionList = new List<IFunction>();

			foreach (IFunction function in functionList)
			{
				IFunction evaluatedFunction = Integrate(function);

				evaluatedFunctionList.Add(evaluatedFunction);
			}

			return evaluatedFunctionList.ToArray();
		}

		/// <summary>Evaluates a function substituting the known functions.</summary>
		/// <param name="function"></param>
		/// <param name="isSimplifyingTopCustomFunction">
		/// If <c>true</c>, it will strip top custom function, e.g. <c>x = 4</c> will become <c>4</c>.
		/// If <c>false</c>, it keeps the top custom function (but will simplify inner ones).
		/// </param>
		/// <returns></returns>
		public IFunction Evaluate(IFunction function, bool isSimplifyingTopCustomFunction)
		{
			IFunction parameterIdentifiedFunction = FunctionHelper.ApplyVisitor(function, new IdentifyUnboundParameterVisitor());
			IFunctionVisitor unscopeFunctionVisitor = new UnscopeFunctionVisitor(_functionIndex.Values.ToArray());
			IFunction unscopedFunction = FunctionHelper.ApplyVisitor(parameterIdentifiedFunction, unscopeFunctionVisitor);

			if (!isSimplifyingTopCustomFunction)
			{
				CustomFunction customFunction = unscopedFunction as CustomFunction;

				if (customFunction != null)
				{
					IFunction evaluatedBody =
						InternalEvaluate(customFunction.FunctionBody, unscopedFunction.ParameterNames);
					IFunction resultingBody = evaluatedBody == null ? customFunction.FunctionBody : evaluatedBody;
					FunctionReference functionReferenceBody = resultingBody as FunctionReference;

					if (functionReferenceBody != null && unscopedFunction.ParameterNames.Length == 0)
					{	//	We change the fct ref to a bound function call to the refered function
						var paramValues = from paramName in functionReferenceBody.InnerFunction.ParameterNames
										  select new UnboundFunctionCalling(paramName, true);
						BoundFunctionCalling functionCall =
							new BoundFunctionCalling(functionReferenceBody.InnerFunction, paramValues.ToArray());
						IFunction evaluatedFunction = new CustomFunction(
							functionCall,
							unscopedFunction.FunctionName,
							functionReferenceBody.InnerFunction.ParameterNames,
							functionReferenceBody.InnerFunction.DefaultParameterValues);

						return evaluatedFunction;
					}
					else
					{
						var factory = FunctionHelper.FunctionParameterBindingFactory;
						var evaluatedDefaultValues =
							from binding in unscopedFunction.DefaultParameterValues
							select factory.Build(binding.Name, Evaluate(binding.Value, true));
						IFunction evaluatedFunction = new CustomFunction(
							evaluatedBody,
							unscopedFunction.FunctionName,
							unscopedFunction.ParameterNames,
							evaluatedDefaultValues.ToArray());

						return evaluatedFunction;
					}
				}
			}

			return InternalEvaluate(unscopedFunction);
		}

		private IFunction InternalEvaluate(IFunction function, params string[] unknownFunctionNameList)
		{
			IFunctionVisitor bindFunctionVisitor =
				new BindFunctionVisitor(_functionIndex, BindingFailureMode.TolerateParameter, unknownFunctionNameList);
			IFunction boundFunction = FunctionHelper.ApplyVisitor(function, bindFunctionVisitor);
			IFunction simplifiedFunction = FunctionHelper.ApplySimplification(SimplificationMode.PerfectEquivalent, boundFunction);

			return simplifiedFunction;
		}
	}
}